Skip to content

[feat][node] Generalize request correlation - #124

Open
TheP2P (thep2p) wants to merge 10 commits into
mainfrom
thep2p/88-request-correlation
Open

[feat][node] Generalize request correlation#124
TheP2P (thep2p) wants to merge 10 commits into
mainfrom
thep2p/88-request-correlation

Conversation

@thep2p

@thep2p TheP2P (thep2p) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Generalizes BaseNode request correlation so one nonce-keyed map serves every message
type, and adds the phase-0 join bootstrap query on top of it.

  • src/node/waiter.rs (new). Waiter carries one variant per message type, a
    blocking SyncSender for search_by_id and a oneshot::Sender for get_max_level,
    because the two callers differ in concurrency shape. WaiterGuard ties map cleanup to
    request scope via Drop, the only cleanup an async caller cannot skip by dropping its
    future mid-await.
  • get_max_level. Asks an introducer for its highest populated lookup-table level
    under a caller-supplied timeout.
  • Routing. RetMaxLevelOp resolves by nonce, and a wrong-variant or expired entry is
    a no-op.

Tests cover the single-request resolve, the timeout plus map cleanup, and a blocking and
an async waiter live in the map at once.

Closes #88

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Generalizes node request correlation to support multiple response types.

Changes:

  • Adds typed search and max-level waiters with RAII cleanup.
  • Implements asynchronous max-level requests and response routing.
  • Adds concurrency and timeout tests; updates private-file ignores.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 5 comments.

File Description
src/node/waiter.rs Defines waiter variants and cleanup guard.
src/node/base_node.rs Adds max-level requests, routing, and tests.
src/node/mod.rs Registers the waiter module.
.gitignore Renames the private-material ignore path.
Suppressed comments (1)

src/node/base_node.rs:305

  • This similarly removes any waiter before checking its kind. A RetMaxLevelOp with a live search nonce silently evicts the Search sender, so the blocking search fails instead of remaining pending. Preserve entries whose variant does not match this response type.
                let waiter = self
                    .request_id_map
                    .lock()
                    .expect("mutex was poisoned by a previous panic")
                    .remove(&res.nonce);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/node/base_node.rs Outdated
Comment on lines +195 to +200
if let Err(e) = self.net.send_event(
introducer,
GetMaxLevelOp(MaxLevelReq {
nonce,
origin: self.core.id(),
}),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct that the arm is missing today, but that's intentional scope for this PR, not an oversight. get_max_level is still #[allow(dead_code)] with a TODO naming the follow-up wiring, its own doc comment calls it a latency optimization rather than a correctness requirement, and the introducer-side handler (computing the highest populated level plus the RetMaxLevelOp reply) is tracked as separate follow-up work sequenced after this one. This PR's scope is the request-correlation mechanism (the asking side), not the new Core operation the responder needs.

Comment thread src/node/base_node.rs Outdated
Comment on lines 283 to 287
@@ -209,10 +285,38 @@ impl EventProcessorCore for BaseNode {
.lock()
.expect("mutex was poisoned by a previous panic")
.remove(&res.nonce);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: only remove the map entry when it's actually a Search waiter, so a mismatched-variant response no longer evicts a live MaxLevel waiter.
e76c978

The mirrored case at line 305 (RetMaxLevelOp evicting a live Search waiter) had the same bug, fixed separately:
8080881

Comment thread src/node/base_node.rs Outdated
Comment on lines +172 to +179
#[allow(dead_code)] // TODO: remove once phase-0 bootstrap is wired into join orchestration.
pub(crate) async fn get_max_level(
&self,
introducer: Identifier,
timeout: Duration,
) -> anyhow::Result<LookupTableLevel> {
let span = tracing::trace_span!("get_max_level", introducer = ?introducer);
let _enter = span.enter();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: wraps the future in .instrument(span) instead of holding enter() across the await, matching the pattern already used in core/context/mod.rs.
df3446e

Comment thread src/node/base_node.rs Outdated
Comment on lines +191 to +193
// cleans up the map entry on every exit path, including cancellation. Never read
// (its only job is running `Drop` at end of scope), hence the `_` prefix.
let _guard = WaiterGuard::new(nonce, self.request_id_map.clone());

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a cancellation test: spawns get_max_level, aborts it mid-flight before any reply is delivered, and asserts the waiter-map entry is cleaned up.
8cda86c

Comment thread src/node/waiter.rs Outdated
@thep2p

Copy link
Copy Markdown
Collaborator Author

Claude (@claude) review

@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude encountered an error —— View job


I'll analyze this and get back to you.

@claude

claude Bot commented Aug 16, 2026

Copy link
Copy Markdown

Claude finished TheP2P (@thep2p)'s task —— View job


Code Review

Reviewed the request-correlation generalization. Overall this is well-crafted work: the Waiter/WaiterGuard design is sound, the concurrency reasoning is careful, and the doc comments explain the why (span instrumentation, drop-on-cancel cleanup, poisoned-lock handling) unusually well. All five prior Copilot comments have been addressed. A few observations below.

Correctness / design

  1. Variant-preserving eviction is now correct (base_node.rs:294-298 and 314-324). Both response arms now get()-and-check the variant before remove(), so a mismatched-nonce response no longer evicts a live sibling waiter. Good fix, and mirrored consistently on both arms.

  2. GetMaxLevelOp responder arm is intentionally absent — agreed this is acceptable for this PR's scope (the asking side). get_max_level is #[allow(dead_code)] with a TODO, and the doc frames it as a latency optimization. One suggestion: the incoming GetMaxLevelOp currently falls through to the _ => arm which returns Err("unsupported event payload type") (base_node.rs:340-343). Since the responder is deliberately deferred, consider whether an introducer receiving this today should hard-error vs. log-and-noop. As-is it's fine, but worth a tracking note so the follow-up doesn't leave it erroring in a live topology.

Minor / nits

  1. waiter.rs:8-14 — the doc has a sentence-fragment break: "...not a map per type. Because the lock protects one logical entity, ...". The Because clause is left dangling. Minor prose cleanup.

  2. search_by_id cleanup asymmetry (base_node.rs:119-146) — the blocking path still uses manual .remove() in each error branch rather than a WaiterGuard. The waiter.rs doc explicitly calls this out as intentional ("search_by_id's existing manual removals are left as-is"), so this is a documented choice, not a defect. Noting only that a future unification could reduce the two cleanup styles to one.

  3. send_event failure on the max-level path (base_node.rs:199-207) returns before the _guard goes out of scope — good, the guard still cleans up the just-inserted entry on this early return. Verified the entry is inserted (line 193) before the guard is created (line 197), so drop order covers it.

Tests

  • test_get_max_level_resolves, test_get_max_level_times_out_and_cleans_up, test_concurrent_requests_of_different_types_resolve_independently, and the new test_get_max_level_cleans_up_on_cancellation give strong coverage: single resolve, timeout+cleanup, sibling-independence, and drop-on-abort. The cancellation test correctly polls the shared map (not just nonce capture) to confirm registration before aborting, and uses bounded timeout loops rather than sleeps — consistent with the project's async-wait convention.
  • Nice touch confirming request_id_map.is_empty() after both timeout and abort paths, which is exactly what proves the WaiterGuard did its job.

Unrelated diffs: identifier.rs (drops redundant & on hex::encode args) and .gitignore (internal/.brain/) are cosmetic and harmless, though technically out of this PR's stated scope.

Nothing blocking. LGTM once the minor prose nit (#3) is optionally cleaned up.

Note: I was unable to run cargo test/clippy in this environment (command approval not granted), so the above is from static review only.
• Branch: thep2p/88-request-correlation

@thep2p

Copy link
Copy Markdown
Collaborator Author

Fixed the dangling sentence in the Waiter doc comment: 9bea5ab

The other item (GetMaxLevelOp falling through to a hard error) is accurate as described but not actionable right now, there's no production transport or live caller yet, and the real handler is already tracked by #91/#86, sequenced right after this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Node] Generalize request correlation: add Phase 0 join bootstrap

2 participants